using System;
using System.Diagnostics;
using System.IO;
using System.Threading;

namespace Atomic.Libraries
{
	/// <summary>
	/// Advisory file lock. Works across NFS file systems. Safe if all processes uses this implementation; Linux doesn't support mandatory file locks.
	/// </summary>
	[DebuggerDisplay("{DebuggerDisplay}")]
	public class FileLock : IDisposable
	{
		private FileStream stream;

		public FileLock(string lockFileName)
			: this(new FileInfo(lockFileName))
		{
		}

		public FileLock(FileInfo lockFile)
		{
			LockFile = lockFile;
		}

		~FileLock()
		{
			Dispose(false);
		}

		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		protected virtual void Dispose(bool disposing)
		{
			// Following this dispose pattern: http://stackoverflow.com/a/898867

			if (stream != null)
			{
				Release();
			}
		}

		public bool TryAcquire()
		{
			if (stream != null)
			{
				throw new InvalidOperationException();
			}

			try
			{
				stream = new FileStream(LockFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None);
				stream.Lock(0, 1);
				return true;
			}
			catch (IOException)
			{
				if (stream != null)
				{
					stream.Dispose();
					stream = null;
				}
				return false;
			}
		}

		public void Acquire()
		{
			// Hopefully a different sequence is generated for each of the processes waiting.
			Random r = new Random();

			while (true)
			{
				if (TryAcquire())
				{
					break;
				}

				// Be gentle to the system; no busy waiting.
				Thread.Sleep(r.Next(50, 100)); 
			}
		}

		public static FileLock Acquire(string lockFileName)
		{
			return Acquire(new FileInfo(lockFileName));
		}

		public static FileLock Acquire(FileInfo lockFile)
		{
			FileLock fileLock = new FileLock(lockFile);
			fileLock.Acquire();
			return fileLock;
		}

		public void Release()
		{
			if (stream == null)
			{
				throw new InvalidOperationException();
			}

			stream.Unlock(0, 1);
			stream.Close();
			stream = null;

			// This isn't strictly required.
			try
			{
				LockFile.Delete();
			}
			catch (FileNotFoundException)
			{
				// Mono seems to throw an exception here if the file was deleted, contrary to what the MSDN documentation says.
			}
		}

		public FileInfo LockFile
		{
			get;
			private set;
		}

		private string DebuggerDisplay
		{
			get
			{
				return LockFile.FullName;
			}
		}
	}
}
